
// todo:

// NEC codes: http://www.sbprojects.com/knowledge/ir/nec.php  
// http://www.arduino.cc/playground/Main/TimerPWMCheatsheet
// http://www.arduino.cc/playground/Code/Timer1  (useful library)
// http://www.atmel.com/dyn/resources/prod_documents/doc8161.pdf (ATMega328 datasheet)
// http://asynclabs.com/wiki/index.php?title=WiServer
// http://www.sics.se/~adam/old-uip/uip-1.0-refman/

#include <EEPROM.h>

#include "IRremote.h"

#include <Servo.h> 

// IR Emitter must be on pin 3 (hardcoded in library)
// Servos must be on a PWM pin (3, 5, 6, 9, 10, 11)
// Pin 13 has a hardwired LED so is not suitable to drive other LEDs
//  (even though blink can be turned off)

// Servo class uses Timer1 and associated interrupts (11-14)
// IRRemote class uses Timer2 and associated interrupts (8-10)
//    (timer on output for PWM and on input to generate events, hence they can't be enabled at the same time)
// WiFi uses Timer0 and associated overflow interrupt, along with PCINT0

#define PIN_SERIAL_RX         0
#define PIN_SERIAL_TX         1
#define PIN_IR_RX             2
#define PIN_IR_TX             3
#define PIN_LED_PAN_LEFT      7
#define PIN_LED_PAN_RIGHT     6
#define PIN_LED_TILT_UP       5
#define PIN_LED_TILT_DOWN     4
#define PIN_TRAINING          8
#define PIN_SERVO_TILT        9
#define PIN_SERVO_PAN        10
#define PIN_LED_ZOOM_IN      11
#define PIN_LED_ZOOM_OUT     12

#define ACTION_PAN_LEFT   1
#define ACTION_PAN_RIGHT  2
#define ACTION_TILT_UP    3
#define ACTION_TILT_DOWN  4
#define ACTION_ZOOM_IN    5
#define ACTION_ZOOM_OUT   6

#define MAX_SERIAL_BUF_LENGTH 20

#define SERVO_INCREMENT   20  // in microseconds
#define IR_FUZZ            5  // in 50 microsecond ticks
#define TRAINING_TIMEOUT  15000  // in microseconds

#define BLINK_TIME 5000  

#define ACK   '#'

char serial_buf[MAX_SERIAL_BUF_LENGTH];
int serial_buf_length;

IRrecv irrecv(PIN_IR_RX);
IRsend irsend;

// servos use timer1
Servo servo_pan;
Servo servo_tilt;

decode_results results;

int training_action;
int training_state;
unsigned long training_start_time;

unsigned long blink_timer;
int blink_pin;

int curr_zoom;
float servo_scale;

void setup() {

  int minpos, maxpos, defaultpos;

  pinMode(PIN_LED_TILT_UP, OUTPUT);
  pinMode(PIN_LED_TILT_DOWN, OUTPUT);
  pinMode(PIN_LED_PAN_LEFT, OUTPUT);
  pinMode(PIN_LED_PAN_RIGHT, OUTPUT);
  pinMode(PIN_LED_ZOOM_IN, OUTPUT);
  pinMode(PIN_LED_ZOOM_OUT, OUTPUT);

  pinMode(PIN_TRAINING, INPUT);
  digitalWrite(PIN_TRAINING, HIGH);  // enable pull-up resistor

  training_action = 0;
  training_state = 0;

  servo_scale = float(MAX_PULSE_WIDTH - MIN_PULSE_WIDTH) / 180.0;  //microseconds per degree

  servo_pan.attach(PIN_SERVO_PAN);
  read_range_from_eeprom('p', &minpos, &maxpos, &defaultpos);
  execute_servo_command('p','=', defaultpos, 'u');  

  servo_tilt.attach(PIN_SERVO_TILT);
  read_range_from_eeprom('t', &minpos, &maxpos, &defaultpos);
  execute_servo_command('t', '=', defaultpos, 'u');

  read_range_from_eeprom('z', &minpos, &maxpos, &defaultpos);
  curr_zoom = maxpos;  // assume zoomed all the way in
  execute_zoom_command('z', '-', maxpos, 'u');  // zoom all the way out
  curr_zoom = 0;
  execute_zoom_command('z', '=', defaultpos, 'u');
  
  emit_external_display_code();

  Serial.begin(9600);	// opens UART serial port on pins 0 (RX) and 1 (TX), sets data rate to 9600 bps

  irrecv.enableIRIn();     

}

void loop() {

  process_training_pin();

  if (Serial.available() > 0) {
    process_serial_input();
  }

  if (irrecv.decode(&results)) {
    process_incoming_ir_code(training_action, &results);
    irrecv.resume(); // Receive the next value
  } 

  show_status_lights();  
}

void process_training_pin() {

  if (digitalRead(PIN_TRAINING) == LOW) {
    training_state = 1;
  } 
  else {
    if (training_state == 1) {  // i.e. trailing edge of pulse
      Serial.print(F("training initiated\n"));
      training_state = 0;
      training_action = (training_action + 1) % 7; 
      Serial.print(F("now training: "));
      Serial.print(training_action);
      Serial.print("\n");

      if (training_action > 0) {
        training_start_time = millis();
      }
    } 
    else {
      if ((training_action > 0) && (millis() > training_start_time + TRAINING_TIMEOUT)) {
        Serial.print(F("training expired\n"));
        training_action = 0;  // training timed out 
      }
    }
  }
} 

void process_serial_input() {
  // implement our own buffering and flow control to avoid blocking and/or overruns
  char incoming_byte;
  while (Serial.available() > 0) {
    incoming_byte = Serial.read();
    Serial.print(incoming_byte);  // echo
    if (incoming_byte == '\n') {
      execute_command(serial_buf);
      serial_buf[0] = '\0';
      serial_buf_length = 0;
      Serial.print("\n> ");
      break;
    } 
    else {  
      if (incoming_byte != '\r') {
        if (serial_buf_length < MAX_SERIAL_BUF_LENGTH - 1) {
          serial_buf[serial_buf_length++] = incoming_byte;
          serial_buf[serial_buf_length] = '\0';
        }
      }
    }
  } 
} 

void process_incoming_ir_code(int &training_action, decode_results *results) {
  Serial.print(F("IR code: "));
  dump(results);
  Serial.print(F("\n"));

  if (training_action > 0) {
    write_ir_code_to_eeprom(training_action, results);
    printIntHex("Training complete for action", training_action);
    training_action = (training_action + 1) % 7;
  } 
  else {
    int action = find_ir_code(results);
    switch (action) {
    case ACTION_PAN_LEFT:
      execute_command("p-1d");              
      break;
    case ACTION_PAN_RIGHT:
      execute_command("p+1d");              
      break;
    case ACTION_TILT_UP:
      execute_command("t+1d");              
      break;
    case ACTION_TILT_DOWN:
      execute_command("t-1d");              
      break;
    case ACTION_ZOOM_IN:
      execute_command("z+1d");              
      break;
    case ACTION_ZOOM_OUT:
      execute_command("z-1d");              
      break;
    }
  }
}

void execute_command(char *buf) {

  Serial.print(F("Processing command: "));
  Serial.print(buf);
  Serial.print("\n");

  char *bufp = buf;

  if (*bufp == '\0')
    return;

  int command = *bufp++;

  if (command == 'n') {
    return;
  }

  if (command == 'h') {
    print_help();
    return;
  }

  if (command == 's') {
    print_status(); 
    return;
  }

  char ref = *bufp++;

  if ((ref != '+') && (ref != '-') && (ref != '=') && (ref != '<') && (ref != '>') && (ref != '@')) {
    Serial.print(F("Invalid parameter: \n"));
    Serial.print(ref);
    Serial.print(F("\n"));
    return;
  }

  int pos = 0;
  while ((*bufp >= '0') && (*bufp <= '9')) {
    pos = pos * 10 + *bufp - '0';
    bufp++;
  }

  char scale = *bufp++;

  if ((scale != 'd') && (scale != 'u')) {
    Serial.print(F("Invalid scale\n"));
    return;
  }

  if (*bufp != '\0') {
    Serial.print(F("Extra characters:"));
    Serial.print(bufp);
    Serial.print("\n");
    return;
  }  

  int delta;  
  switch (command) {
  case 'p':
    delta = execute_servo_command(command, ref, pos, scale);
    if (delta < 0) {
      blink_pin = PIN_LED_PAN_LEFT;
    } 
    else if (delta > 0) {
      blink_pin = PIN_LED_PAN_RIGHT;
    }
    break;
  case 't': 
    delta = execute_servo_command(command, ref, pos, scale);
    if (delta < 0) {
      blink_pin = PIN_LED_TILT_DOWN;
    } 
    else if (delta > 0) {
      blink_pin = PIN_LED_TILT_UP;
    }
    break;
  case 'z':
    delta = execute_zoom_command(command, ref, pos, scale);
    if (ref == '+') {
      blink_pin = PIN_LED_ZOOM_IN;
    } 
    else {
      blink_pin = PIN_LED_ZOOM_OUT;
    }
    break;
  default:
    Serial.print(F("Invalid command\n"));
    return;
  }

  blink_timer = BLINK_TIME;
}

int execute_servo_command(char command, char ref, int pos, char scale) {

  Servo s;
  int minpos, maxpos, defaultpos;

  if (command == 'p') {
    s = servo_pan;
  } 
  else {
    s = servo_tilt; 
  }

  if (scale == 'd') {
    pos = pos * servo_scale;
  }

  int prev_pos = s.readMicroseconds();

  switch (ref) {
  case '-':
    pos = prev_pos - pos;
    break;
  case '+':
    pos = prev_pos + pos;
    break;
  case '=':
    if (scale == 'd') {
      pos += MIN_PULSE_WIDTH;
    }
    break;
  case '>':
  case '<':
  case '@':
    if (scale == 'd') {
      pos += MIN_PULSE_WIDTH;
    }
    write_range_to_eeprom(command, ref, pos);
    return 0;
  default:
    Serial.print(F("Invalid parameter: "));
    Serial.print(ref);
    Serial.print(F("\n"));
    return 0;
  }

  read_range_from_eeprom(command, &minpos, &maxpos, &defaultpos);

  int unconstrained_pos = pos;
  pos = constrain(pos, minpos, maxpos);
  if (pos != unconstrained_pos) {
    Serial.print(F("Requested position out of range: "));
    Serial.print(unconstrained_pos);
    Serial.print(F("\n"));
  }

  Serial.print(F("Moving servo to: "));
  Serial.print(pos);
  Serial.print(F("\n"));

  s.writeMicroseconds(pos);

  return pos - prev_pos;
} 

int execute_zoom_command(char command, char ref, int pos, char scale) {
  int action;
  int prev_zoom;
  int minpos, maxpos, defaultpos;
  int delta;

  prev_zoom = curr_zoom;

  decode_results eeprom_ir_code;
  volatile unsigned int eeprom_ir_code_rawbuf[RAWBUF];
  eeprom_ir_code.rawbuf = &eeprom_ir_code_rawbuf[0]; 

  switch (ref) {
  case '-':
    curr_zoom -= pos;
    break;
  case '+':
    curr_zoom += pos;
    break;
  case '=':
    curr_zoom = pos;
    break;
  case '>':
  case '<':
  case '@':
    write_range_to_eeprom(command, ref, pos);
    return 0;
  default:
    Serial.print(F("Invalid parameter: "));
    Serial.print(ref);
    Serial.print(F("\n"));
    return 0;
  }

  read_range_from_eeprom(command, &minpos, &maxpos, &defaultpos);

  int unconstrained_zoom = curr_zoom;
  curr_zoom = constrain(curr_zoom, minpos, maxpos);
  if (curr_zoom != unconstrained_zoom) {
    Serial.print(F("Requested position out of range: "));
    Serial.print(unconstrained_zoom);
    Serial.print(F("\n"));
  }

  if (curr_zoom < prev_zoom) {
    action = ACTION_ZOOM_OUT;
    delta = prev_zoom - curr_zoom;
  } 
  else {
    action = ACTION_ZOOM_IN;
    delta = curr_zoom - prev_zoom;
  }

  read_ir_code_from_eeprom(action, &eeprom_ir_code);
  
  for (int i=0; i<delta; i++) {
    emit_ir_code(&eeprom_ir_code);
  }

  return delta;
}

void print_status() {

  int minpos, maxpos, defaultpos;

  read_range_from_eeprom('p', &minpos, &maxpos, &defaultpos);
  Serial.print(F("Pan range: "));
  Serial.print(u2d(minpos));
  Serial.print(F("-"));
  Serial.print(u2d(maxpos));
  Serial.print(F("  default: "));
  Serial.print(u2d(defaultpos));
  Serial.print(F("  current: "));
  Serial.print(u2d(servo_pan.readMicroseconds()));
  Serial.print(F("\n"));

  read_range_from_eeprom('t', &minpos, &maxpos, &defaultpos);
  Serial.print(F("Tilt range: "));
  Serial.print(u2d(minpos));
  Serial.print(F("-"));
  Serial.print(u2d(maxpos));
  Serial.print(F("  default: "));
  Serial.print(u2d(defaultpos));
  Serial.print(F("  current: "));
  Serial.print(u2d(servo_tilt.readMicroseconds()));
  Serial.print(F("\n"));

  read_range_from_eeprom('z', &minpos, &maxpos, &defaultpos);
  Serial.print(F("Zoom range: "));
  Serial.print(minpos);
  Serial.print(F("-"));
  Serial.print(maxpos);
  Serial.print(F("  default: "));
  Serial.print(defaultpos);
  Serial.print(F("  current: "));
  Serial.print(curr_zoom);
  Serial.print(F("\n"));

  for (int i = 1; i < 7; i++) {
    dump_ir_code(i);
  }
}


void show_status_lights() {
  int pin = 0;

  if (training_action > 0) {
    switch (training_action) {
    case ACTION_PAN_LEFT:
      pin = PIN_LED_PAN_LEFT;
      break;
    case ACTION_PAN_RIGHT:
      pin = PIN_LED_PAN_RIGHT;
      break;
    case ACTION_TILT_UP:
      pin = PIN_LED_TILT_UP;
      break;
    case ACTION_TILT_DOWN:
      pin = PIN_LED_TILT_DOWN;
      break;
    case ACTION_ZOOM_IN:
      pin = PIN_LED_ZOOM_IN;
      break;
    case ACTION_ZOOM_OUT:
      pin = PIN_LED_ZOOM_OUT;
      break;
    } 
  }
  else {
    if (blink_pin > 0) {
      if (blink_timer > 0) {
        blink_timer -= 1;
        pin = blink_pin;
      }
    }
  }

  digitalWrite(PIN_LED_TILT_UP, LOW);
  digitalWrite(PIN_LED_TILT_DOWN, LOW);
  digitalWrite(PIN_LED_PAN_LEFT, LOW);
  digitalWrite(PIN_LED_PAN_RIGHT, LOW);
  digitalWrite(PIN_LED_ZOOM_IN, LOW);
  digitalWrite(PIN_LED_ZOOM_OUT, LOW);
  digitalWrite(pin, HIGH);
} 


void print_help() {
  Serial.print(F("Commands:\n"));
  Serial.print(F("h                   Display this help information\n"));
  Serial.print(F("s                   Display current status\n"));
  Serial.print(F("p[+|-|=|>|<|@]n[u|d]  Pan\n"));
  Serial.print(F("t[+|-|=|>|<|@]n[u|d]  Tilt\n"));
  Serial.print(F("z[+|-|=|>|<|@]n[u]    Zoom\n"));
  Serial.print(F("\n"));
  Serial.print(F("Parameters:\n"));
  Serial.print(F("+                   Increment by n\n"));
  Serial.print(F("-                   Decrement by n\n"));
  Serial.print(F("=                   Move to n\n"));
  Serial.print(F(">                   Set minimum position to n\n"));
  Serial.print(F("<                   Set maximum position to n\n"));
  Serial.print(F("@                   Set default position to n\n"));
  Serial.print(F("u                   Internal units (e.g. servo pulse width in microseconds\n"));
  Serial.print(F("d                   Degrees\n"));
  Serial.print(F("\n"));
  Serial.print(F("Examples:\n"));
  Serial.print(F("p=94d               Pan to 94 degrees\n"));
  Serial.print(F("z+5u                Zoom in 5 units\n"));
  Serial.print(F("t<120d              Set maximum tilt to 120 degrees\n"));
}

void emit_external_display_code() {
  // the Mark I has no way to record this code, so it is hard-coded here

  Serial.print(F("Emitting External Display code\n"));
  
  decode_results ir_code;
  volatile unsigned int ir_code_rawbuf[RAWBUF];
  ir_code.decode_type = PANASONIC;
  ir_code.command[0] = 0x40;
  ir_code.command[1] = 0x04;
  ir_code.command[2] = 0x0E;
  ir_code.command[3] = 0x14;
  ir_code.command[4] = 0xEA;
  ir_code.command[5] = 0xF0;
  ir_code.command[6] = 0x00;
  ir_code.command[7] = 0x00;
  ir_code.value = 0;
  ir_code.bits = 48;
  ir_code.rawbuf = &ir_code_rawbuf[0]; 
  ir_code.rawlen = 0;

  emit_ir_code(&ir_code);
}






































